<?php
/**
 * Created by PhpStorm.
 * User: caoyangmin
 * Date: 16/9/27
 * Time: 下午3:23
 */

namespace Once;
use Laravel\Lumen\Application;
use Once\Annotations\AnnotationHandler;
use Once\Annotations\BaseAnnotationHandler;
use Once\Container\ControllerContainer;
use Once\Container\EntityContainer;
use Once\Utils\AnnotationsVisitor;
use Once\Utils\FileExpiredChecker;
use Once\Utils\LocalCache;
use Once\Utils\Logger;
use Once\Utils\Verify;

/**
 * Class SerializableException
 * @package Once
 * 用于保存路由加载失败后的异常, 以便通过缓存能够获取上次加载失败的原因
 */
class SerializableException{

    public function __construct(\Exception $exception){
        $this->name = get_class($exception);
        $this->code = $exception->getCode();
        $this->message = $exception->getMessage();
    }

    /**
     * 重新抛出异常, 但只保留异常的code和message
     */
    public function rethrow(){
        $name = $this->name;
        throw new $name($this->message, $this->code);
    }

    private $name;
    private $code;
    private $message;
}
//TODO **   支持路由缓存预热
/**
 * Class RouteLoader
 * @package Once
 *
 */
class Api
{
    /**
     * @param string $className
     * @return ControllerContainer
     */
    public function getRoutesFromClass($className){
        $cache = new LocalCache();
        $key = md5(getcwd().$className.__METHOD__.'v1'); //加getcwd()只是为了让不同项目能产生不同的key
        $success = false;
        $res = $cache->get($key, $success);
        if($success){
            if($res instanceof SerializableException){
                $res->rethrow();
            }
            return $res;
        }
        $refl = new \ReflectionClass($className) or Verify::fail("load class $className failed");
        $fileName = $refl->getFileName();
        //读缓存
        //加锁,防止重复解析(高并非环境下可能导致雪崩)
        $lock = $key.'_lock';
        !$cache->get($lock) or Verify::fail('Api::getRoutesFromClass() failed, loading is going on');
        $cache->set($lock, 1, 60) or Verify::fail('Api::getRoutesFromClass() failed, lock failed'); //TODO, 此处加锁并非原子性

        try{
            $res = $this->getRoutesFromClassWithoutCache($className);
            $cache->set($key, $res,0, new FileExpiredChecker($fileName));
        }catch (\Exception $e){
            $cache->set($key, new SerializableException($e),0, new FileExpiredChecker($fileName));
            $cache->del($lock);
            throw $e;
        }
        $cache->del($lock);
        return $res;
    }
    /**
     * @param string $className
     * @return ControllerContainer
     */
    public function getRoutesFromClassWithoutCache($className){
        $container = new ControllerContainer($className);
        //加载所有注释
        AnnotationsVisitor::visit(
            $className,
            function ($type, $target, $name, $value) use ($container){
                $handler = $this->getAnnotationHandler('Controller', $name, $container);
                if($handler){
                    // TODO 递归子节点
                    if($handler->handle($type, $target, $name, $value)) {
                        AnnotationsVisitor::visitIntoBlock(
                            $value,
                            function ($_, $__, $name, $value) use ($type, $target, $handler, $container) {
                                $child = $this->getAnnotationHandler('Controller', $name, $container, $handler);
                                if($child){
                                    $child->handle($type, $target, $name, $value);
                                }
                            }
                        );
                    }
                }
            }
        );
        return $container;
    }

    /**
     * @param Application $app
     * @param string $className
     * @return ControllerContainer
     */
    public function loadRoutesFromClass(Application $app, $className){
        $container = $this->getRoutesFromClass($className);
        $container->applyRoutes($app);
        return $container;
    }

    /**
     * @param $className
     * @return EntityContainer
     */
    public function getEntity($className){
        $cache = new LocalCache();
        $key = getcwd().$className.__METHOD__.'v1';
        $key = md5($key); //加getcwd()只是为了让不同项目能产生不同的key
        $success = false;
        $res = $cache->get($key, $success);
        if($success){
            if($res instanceof SerializableException){
                $res->rethrow();
            }
            return $res;
        }
        //读缓存
        //加锁,防止重复解析(高并非环境下可能导致雪崩)
        $lock = $key.'_lock';
        !$cache->get($lock) or Verify::fail('Api::getEntity() failed, loading is going on');
        $cache->set($lock, 1, 60) or Verify::fail('Api::getEntity() failed, lock failed'); //TODO, 此处加锁并非原子性
        try{
            $res = $this->getEntityWithoutCache($className);
            $cache->set($key, $res, 0, new FileExpiredChecker($res->getFileName()));
        }catch (\Exception $e){
            $cache->set($key, new SerializableException($e), 0, new FileExpiredChecker($className));
            $cache->del($lock);
            throw $e;
        }
        $cache->del($lock);
        return $res;
    }
    /**
     * @param $className
     * @return EntityContainer
     */
    public function getEntityWithoutCache($className){
        $container = new EntityContainer($className);
        //加载所有注释
        AnnotationsVisitor::visit(
            $className,
            function ($type, $target, $name, $value) use ($container){
                $handler = $this->getAnnotationHandler('Entity',$name,$container);
                if($handler){
                    $handler->handle($type, $target, $name, $value);
                }
            }
        );
        return $container;
    }
    /**
     * 创建实体
     * @param Application $app
     * @param string $className
     * @param array $params
     * @return mixed
     */
    public function makeEntity(Application $app, $className, array $params){
        $container = $this->getEntity($className);
        return $container->make($app, $params);
    }


    /**
     *
     * @param $name $name转类名规则:
     * 1. 去掉前缀o-(若有)
     * 2. 单词首字母大写
     * 3. 去掉所有中横杠
     * 如 o-my-ann 对应的类名是 MyAnn
     *
     * @param string $type
     * @param string $name
     * @param object $container
     * @param object $parent
     * @return AnnotationHandler
     */
    public function getAnnotationHandler($type, $name, $container, BaseAnnotationHandler $parent=null){
        is_string($name) or Verify::fail(new \InvalidArgumentException('$name'));

        $namespace = "\\Once";
        foreach ($this->fixNamespace as $k=>$v){
            if(strlen(ltrim($name, $v['fix'])) != strlen($name)){
                $name = ltrim($name, $v['fix']);
                $namespace = $v['namespace'];
                break;
            }
        }

        // 首字母大写
        $name = ucwords($name, '-');
        $name = str_replace('-', '', $name);
        $name = $namespace . "\\Annotations\\$type\\Ann$name";
        if(!class_exists($name)){
            Logger::debug("unsupported annotation $name");
            return null;
        }
        if($parent){
            return new $name($container, $parent);
        }else{
            return new $name($container);
        }
    }

    protected $fixNamespace = [
        array('fix'=>'o-', 'namespace'=>'\\Once'),
        array('fix'=>'ff-', 'namespace'=>'\\Once\\Ffan')
    ];

    public function addAnnotationHandlerPath($fix, $namespace){
        array_unshift($this->fixNamespace, array('fix'=>$fix.'-', 'namespace'=>$namespace));
    }

    /**
     * @param $fromPath
     * @param $namespace
     * @return ControllerContainer[]
     */
    public function getRoutesFromDir($fromPath , $namespace){

        $dir = @dir($fromPath);

        $geteach = function ()use($dir){
            $name = $dir->read();
            if(!$name){
                return $name;
            }
            return $name;
        };

        $items=[];
        while( !!($entry = $geteach()) ){
            if($entry == '.' || $entry=='..'){
                continue;
            }
            $path = $fromPath.'/'. str_replace('\\', '/', $entry);
            if(is_file($path) && substr_compare ($entry, '.php', strlen($entry)-4,4,true) ==0){
                $class_name = $namespace.'\\'.substr($entry, 0, strlen($entry)-4);
                try{
                    $item = $this->getRoutesFromClass($class_name);
                    $items[] = $item;
                }catch (\Exception $e){
                    Logger::warning("load route from $class_name failed with: ".get_class($e).' '.$e->getMessage());
                }
            }else{
                Logger::debug($path.' ignored');
            }
        }
        if($dir !== null){
            $dir->close();
        }
        return $items;

    }

    /**
     * @param $fromPath
     * @param ControllerContainer[]
     */
    public function loadRoutesFromDir($fromPath , $namespace){
        $routes = $this->getRoutesFromDir($fromPath , $namespace);
//        foreach($routes as $route){
//            $route->applyRoutes($app);
//        }
        return $routes;
    }

    /**
     * TODO 使用自定义注释
     * @param string $controllerAnn 控制器注释扫描路径
     * @param string  $entityAnn 实体注释烧卖路径
     * @param string  $namespace 名字空间
     * @return $this
     */
    public function withExternalAnnotations($controllerAnn, $entityAnn, $namespace){
        return $this;
    }

    /**
     * @return static
     */
    static public function getInstance(){
        static $thiz = null;
        if(!$thiz){
            $thiz = new static();
        }
        return $thiz;
    }
}